package jxtend.processor.eclipse.visitor;

import jxtend.processor.reflect.ReflectField;
import jxtend.processor.reflect.ReflectMethod;
import jxtend.processor.reflect.ReflectMethod.VoidMethod1Args;
import jxtend.processor.reflect.ReflectMethod.VoidMethod2Args;

import org.eclipse.jdt.internal.codeassist.complete.CompletionOnAnnotationOfType;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Clinit;
import org.eclipse.jdt.internal.compiler.ast.QualifiedAllocationExpression;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.eclipse.jdt.internal.compiler.lookup.MethodScope;
import org.eclipse.jdt.internal.compiler.lookup.PackageBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;

@SuppressWarnings("restriction")
public final class ASTNodeScopeFactory {
	private static final VoidMethod1Args<ClassScope, SourceTypeBinding> buildLocalTypeBindingMethod =
			ReflectMethod.voidMethod(ClassScope.class, "buildLocalTypeBinding", SourceTypeBinding.class);
	private static final VoidMethod2Args<ClassScope, SourceTypeBinding, ReferenceBinding>
		buildAnonymousTypeBindingMethod = ReflectMethod.voidMethod(
				ClassScope.class, "buildAnonymousTypeBinding", SourceTypeBinding.class, ReferenceBinding.class);

	public static Scope create(
			ASTNode node, ReflectField<? extends ASTNode, ? extends Scope> scopeFieldOfNode, Scope parentScope) {
		if (parentScope instanceof MethodScope && node instanceof TypeDeclaration
				&& (((TypeDeclaration) node).bits & ASTNode.IsLocalType) != 0
				&& (((TypeDeclaration) node).bits & ASTNode.IsAnonymousType) == 0) {
			TypeDeclaration typeDeclaration = (TypeDeclaration) node;
			//TODO: find out why I have to explicitly add the default constructor
//			typeDeclaration.addClinit();

			ClassScope localClassScope = new ClassScope((BlockScope) parentScope, typeDeclaration);
			buildLocalTypeBindingMethod.invoke(localClassScope, parentScope.enclosingSourceType());

			return localClassScope;
		} else if (parentScope instanceof BlockScope) {
			if (scopeFieldOfNode.field.getType().equals(BlockScope.class)) {
				return new BlockScope((BlockScope) parentScope);
			} else {
				assert scopeFieldOfNode.field.getType().equals(ClassScope.class) && (node instanceof TypeDeclaration);

				TypeDeclaration typeDeclaration = (TypeDeclaration) node;
				if (typeDeclaration.allocation instanceof QualifiedAllocationExpression) {
					QualifiedAllocationExpression qualifiedAllocationExpression =
						(QualifiedAllocationExpression) typeDeclaration.allocation;

					typeDeclaration.bits |= ASTNode.IsAnonymousType;

					ReferenceBinding superBinding;
					if (qualifiedAllocationExpression.enumConstant == null) {

						if (qualifiedAllocationExpression.type.resolvedType == null) {
							// resolve the type reference of the anonymous allocation expression
							qualifiedAllocationExpression.type.resolve((BlockScope) parentScope);
						}

						superBinding = (ReferenceBinding) qualifiedAllocationExpression.type.resolvedType;

//						//TODO: FIX issue; problem is that the ASTNodeScopeFactory shouldn't really be used for
//						//	beginVisit (in this case the SAM handler still has to be called) but only for endVisit
//						//  (BOTH before and after resolve)
//						if (superBinding == null) {
//							superBinding = new ProblemReferenceBinding(
//									qualifiedAllocationExpression.type instanceof QualifiedTypeReference
//										? ((QualifiedTypeReference) qualifiedAllocationExpression.type).tokens
//										: new char[][] {
//												((SingleTypeReference) qualifiedAllocationExpression.type).token },
//									null, ProblemReasons.NotFound);
//						}
					} else {
						superBinding = ((BlockScope) parentScope).referenceType().binding;
					}

					// from BlockScope.addAnonymousType(TypeDeclaration anonymousType, ReferenceBinding)
					ClassScope anonymousClassScope = new ClassScope((BlockScope) parentScope, typeDeclaration);

					buildAnonymousTypeBindingMethod.invoke(
							anonymousClassScope, parentScope.enclosingSourceType(), superBinding);
					return anonymousClassScope;
				}
			}
		} else if (parentScope instanceof ClassScope) {
			if (node instanceof AbstractMethodDeclaration) {
				AbstractMethodDeclaration abstractMethodDeclaration = (AbstractMethodDeclaration) node;
				ClassScope classScope = (ClassScope) parentScope;

				boolean isStatic = (abstractMethodDeclaration instanceof Clinit)
					? true : (abstractMethodDeclaration.modifiers & ClassFileConstants.AccStatic) != 0;
				return new MethodScope(classScope, classScope.referenceContext, isStatic);
			}
		} else if (parentScope instanceof CompilationUnitScope) {
			if (node instanceof TypeDeclaration) {
				TypeDeclaration typeDeclaration = (TypeDeclaration) node;

				ClassScope result =  new ClassScope(parentScope, typeDeclaration);

				if (node instanceof CompletionOnAnnotationOfType) {
					CompletionOnAnnotationOfType completionOnAnnotationOfType = (CompletionOnAnnotationOfType) node;
					if (completionOnAnnotationOfType.potentialAnnotatedNode instanceof TypeDeclaration) {
						PackageBinding packageBinding = ((CompilationUnitScope) parentScope).fPackage;

						char[][] compoundName = new char[packageBinding.compoundName.length + 1][];
						System.arraycopy(packageBinding.compoundName, 0,
								compoundName, 0, packageBinding.compoundName.length);
						compoundName[packageBinding.compoundName.length] =
							((TypeDeclaration) completionOnAnnotationOfType.potentialAnnotatedNode).name;

						SourceTypeBinding sourceTypeBinding =
							new SourceTypeBinding(compoundName, packageBinding, result);
						sourceTypeBinding.memberTypes = Binding.NO_MEMBER_TYPES;
						sourceTypeBinding.typeVariables = Binding.NO_TYPE_VARIABLES;
						sourceTypeBinding.scope = result;

						completionOnAnnotationOfType.binding = sourceTypeBinding;

						return result;
					}
				} else {
					return result;
				}
			}
		}

		throw new IllegalArgumentException("Could not create scope");
	}

	private ASTNodeScopeFactory() {
		// Utility class
	}
}
